package com.merry.mybatis;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.InvocationHandler;
import java.util.Properties;

/**
 * 该类继承了 {@link Interceptor}，从而表示该类是一个拦截器实现类。
 * 这个拦截器可以拦截 ：
 * <li>{@link Executor}、专门处理sql的接口。
 * <li>{@link ParameterHandler}、专门处理参数绑定的接口。
 * <li>{@link StatementHandler}、专门处理statement的接口。
 * <li>{@link ResultSetHandler}、专门处理返回结果集的接口。
 * <br>四大接口中的所有方法。
 *
 * <p></p>
 * 那么怎么判断当前拦截器是，拦截的哪个方法呢？通过 {@link Intercepts} 注解，该注解可以写一个 {@link Signature} 集合。
 * {@code Signature}中有三个参数： {@code type}，表示当前拦截器拦截哪一个类（属于上面四大接口之一）。 {@code method}，表示当前拦截器拦截哪个类中的哪个方法。
 * {@code args}，为例解决参数重载。比如 {@code query} 方法就有两个重载方法，就是通过{@code args}来区分重载的。
 *
 * <p> 怎么生效？在Spring中，可以将这个类，交给Spring容器管理，它会自动生效（其实注册给mybatis交给spring容器完成了），或者在通过mybatis的方式注册一个拦截器。
 */
@Intercepts(
        {
            @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        }
)
@Component
public class ParamInterceptor implements Interceptor {

    /**
     * <p>这个方法很好理解：
     * <p>作用只有一个：我们不是拦截方法吗，拦截之后我们要做什么事情呢？这个方法里面就是我们要做的事情。
     * <p>解释这个方法前，我们一定要理解方法参数 {@link Invocation} 是什么？
     *      <li>1：我们知道，mybatis拦截器默认只能拦截四种类型，{@code Executor}、{@code ParameterHandler}、{@code StatementHandler}、{@code ResultSetHandler}
     *      <li>2：不管是哪种代理，代理的目标对象就是我们要拦截的对象，举例说明：
     *          比如我们要拦截 {@link Executor#update(MappedStatement ms, Object paramters)}方法。
     *          那么 {@code Invocation} 就是这个对象，Invocation里面有三个参数。
     *           <li>{@link Invocation#getTarget()} 就是 {@link Executor} 对象</li>
     *           <li>{@link Invocation#getMethod()} 就是 {@link Executor#update} 方法
     *           <li>{@link Invocation#getArgs()} 就是 {@code (MappedStatement ms, Object paramters)}</li>
     * <p>案例代码如下所示</p>
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        /*
         * 需求：我们需要对所有更新操作前打印查询语句的 sql 日志。
         * 那我就可以让我们的自定义拦截器 xxxxInterceptor 拦截 Executor#update 方法，在update之前打印日志。
         * 所以我们的拦截点就是 {@code Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})}
         *
         * 当我们成功打印日志之后，只需要调用 invocation#proceed 方法，继续下面的执行。
         */
        //由于这里我们指定拦截的就是 Executor 类。所以可以直接转换。
        Executor executor = (Executor) invocation.getTarget();
        Object[] args = invocation.getArgs();
        //拿到 MappedStatement 对象，这个对象可以拼接到 sql。
        MappedStatement ms = (MappedStatement) args[0];
        SqlSource sqlSource = ms.getSqlSource();
        BoundSql boundSql = sqlSource.getBoundSql(args);
        String sql = boundSql.getSql();
        System.out.println(sql);
        System.out.println("拦截到了查询的方法。");
        return invocation.proceed();
    }

    /**
     * 这个方法也很好理解：
     * 作用只有一个：那就是 Mybatis 在创建拦截器代理时候会判断一次，当前这个类，到底需不需要生成一次代理对象进行拦截。
     *      <li>如果需要拦截：就生成一个代理对象，这个对象就是一个 {@link Plugin}，它实现了JDK的动态代理接口 {@link InvocationHandler},
     *      <li>如果不需要代理；则直接返回目标对象本身。
     * mybatis为什么会判断一次是否需要代理呢？
     *  <p>默认情况下，mybatis只拦截四种类型的接口，通过 {@link Intercepts}和{@link Signature}注解来指示当前拦截器要具体拦截的方法。
     *  这个方法，就是拦截器生成代理对象的方法，它会判断 当前对象 适不适合生成一个代理对象。比如万一不是mybatis拦截的，则不需要生成拦截方法。
     *
     * 该方法在 mybatis 加载核心配置文件时 被调用。
     */
    @Override
    public Object plugin(Object target) {

        /**
         * 这里的逻辑只有一个，就是让mybatis判断，要不要返回一个代理对象。
         * 从源码可以看出，它就是通过获取 该拦截器上面的注解，判断当前 target，要不要被代理。
         */
        return Plugin.wrap(target,this);
    }

    /**
     * 初始化变量的方法：如果该拦截器想要初始化一些自己的参数，那么调用这个方法就可以了。
     * 不过注意：在SpringBoot中通过 {@code Component} 注解注入进容器生效的，拦截器，这个方法，不生效。可以使用 {@code Value}注解替换。
     * 获取使用 {@code PostConstruct} 代替，反正就是你要使用 Spring提供的一些初始化方式。
     */
    @Override
    public void setProperties(Properties properties) {

    }
}
